/*
* Copyright 2012 GitHub Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gh4a.utils;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.support.annotation.AttrRes;
import android.support.v4.content.ContextCompat;
import android.text.Editable;
import android.text.Html.ImageGetter;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineBackgroundSpan;
import android.text.style.ParagraphStyle;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import com.gh4a.R;
import org.ccil.cowan.tagsoup.HTMLSchema;
import org.ccil.cowan.tagsoup.Parser;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static android.graphics.Paint.Style.FILL;
public class HtmlUtils {
private static class ReplySpan implements LeadingMarginSpan {
private final int mColor;
private final int mMargin;
private final int mSize;
public ReplySpan(int margin, int size, int color) {
mColor = color;
mMargin = margin;
mSize = size;
}
@Override
public int getLeadingMargin(boolean first) {
return mMargin;
}
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom, CharSequence text,
int start, int end, boolean first, Layout layout) {
final Style style = p.getStyle();
final int color = p.getColor();
p.setStyle(FILL);
p.setColor(mColor);
c.drawRect(x, top, x + dir * mSize, bottom, p);
p.setStyle(style);
p.setColor(color);
}
}
private static class CodeBlockSpan implements LineBackgroundSpan {
private final int mColor;
public CodeBlockSpan(int color) {
mColor = color;
}
@Override
public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline,
int bottom, CharSequence text, int start, int end, int lnum) {
final int paintColor = p.getColor();
p.setColor(mColor);
c.drawRect(left, top, right, bottom, p);
p.setColor(paintColor);
}
}
private static class HorizontalLineSpan implements LineBackgroundSpan {
private final int mColor;
private final float mHeight;
public HorizontalLineSpan(float height, int color) {
mColor = color;
mHeight = height;
}
@Override
public void drawBackground(Canvas c, Paint p, int left, int right, int top, int baseline,
int bottom, CharSequence text, int start, int end, int lnum) {
final int paintColor = p.getColor();
final float centerY = (top + bottom) / 2;
p.setColor(mColor);
c.drawRect(left, centerY - mHeight / 2, right, centerY + mHeight / 2, p);
p.setColor(paintColor);
}
}
/**
* Rewrite relative URLs in HTML fetched e.g. from markdown files.
*
* @param html
* @param repoUser
* @param repoName
* @param branch
* @return
*/
public static String rewriteRelativeUrls(final String html, final String repoUser,
final String repoName, final String branch) {
final String baseUrl = "https://raw.github.com/" + repoUser + "/" + repoName + "/" + branch;
final StringBuffer sb = new StringBuffer();
final Pattern p = Pattern.compile("(href|src)=\"(\\S+)\"");
final Matcher m = p.matcher(html);
while (m.find()) {
String url = m.group(2);
if (!url.contains("://") && !url.startsWith("#")) {
if (url.startsWith("/")) {
url = baseUrl + url;
} else {
url = baseUrl + "/" + url;
}
}
m.appendReplacement(sb, Matcher.quoteReplacement(m.group(1) + "=\"" + url + "\""));
}
m.appendTail(sb);
return sb.toString();
}
/**
* Encode HTML
*
* @param html
* @param imageGetter
* @return html
*/
public static CharSequence encode(final Context context, final String html,
final ImageGetter imageGetter) {
if (TextUtils.isEmpty(html))
return "";
return Html.fromHtml(context, html, imageGetter);
}
/* a copy of the framework's HTML class, stripped down and extended for our use cases */
private static class Html {
private Html() { }
/**
* Lazy initialization holder for HTML parser.
*/
private static class HtmlParser {
private static final HTMLSchema schema = new HTMLSchema();
}
public static Spanned fromHtml(Context context,
String source, android.text.Html.ImageGetter imageGetter) {
Parser parser = new Parser();
try {
parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
} catch (org.xml.sax.SAXNotRecognizedException | org.xml.sax.SAXNotSupportedException e) {
// Should not happen.
throw new RuntimeException(e);
}
HtmlToSpannedConverter converter =
new HtmlToSpannedConverter(context, source, imageGetter, parser);
return converter.convert();
}
}
private static class HtmlToSpannedConverter implements ContentHandler {
private static final float[] HEADING_SIZES = {
1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
};
private float mDividerHeight;
private int mBulletMargin;
private int mReplyMargin;
private int mReplyMarkerSize;
private Context mContext;
private String mSource;
private XMLReader mReader;
private SpannableStringBuilder mSpannableStringBuilder;
private android.text.Html.ImageGetter mImageGetter;
private static Pattern sTextAlignPattern;
private static Pattern sForegroundColorPattern;
private static Pattern sBackgroundColorPattern;
private static Pattern sTextDecorationPattern;
private static Pattern getTextAlignPattern() {
if (sTextAlignPattern == null) {
sTextAlignPattern = Pattern.compile("(?:\\s+|\\A)text-align\\s*:\\s*(\\S*)\\b");
}
return sTextAlignPattern;
}
private static Pattern getForegroundColorPattern() {
if (sForegroundColorPattern == null) {
sForegroundColorPattern = Pattern.compile(
"(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
}
return sForegroundColorPattern;
}
private static Pattern getBackgroundColorPattern() {
if (sBackgroundColorPattern == null) {
sBackgroundColorPattern = Pattern.compile(
"(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
}
return sBackgroundColorPattern;
}
private static Pattern getTextDecorationPattern() {
if (sTextDecorationPattern == null) {
sTextDecorationPattern = Pattern.compile(
"(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
}
return sTextDecorationPattern;
}
public HtmlToSpannedConverter(Context context, String source,
android.text.Html.ImageGetter imageGetter, Parser parser) {
final Resources res = context.getResources();
mDividerHeight = res.getDimension(R.dimen.divider_span_height);
mBulletMargin = res.getDimensionPixelSize(R.dimen.bullet_span_margin);
mReplyMargin = res.getDimensionPixelSize(R.dimen.reply_span_margin);
mReplyMarkerSize = res.getDimensionPixelSize(R.dimen.reply_span_size);
mContext = context;
mSource = source;
mSpannableStringBuilder = new SpannableStringBuilder();
mImageGetter = imageGetter;
mReader = parser;
}
public Spanned convert() {
mReader.setContentHandler(this);
//noinspection TryWithIdenticalCatches
try {
mReader.parse(new InputSource(new StringReader(mSource)));
} catch (IOException e) {
// We are reading from a string. There should not be IO problems.
throw new RuntimeException(e);
} catch (SAXException e) {
// TagSoup doesn't throw parse exceptions.
throw new RuntimeException(e);
}
// Replace the placeholders for leading margin spans in reverse order, so the leading
// margins are drawn in order of tag start
Object[] obj = mSpannableStringBuilder.getSpans(0,
mSpannableStringBuilder.length(), NeedsReversingSpan.class);
for (int i = obj.length - 1; i >= 0; i--) {
NeedsReversingSpan span = (NeedsReversingSpan) obj[i];
int start = mSpannableStringBuilder.getSpanStart(span);
int end = mSpannableStringBuilder.getSpanEnd(span);
mSpannableStringBuilder.removeSpan(span);
mSpannableStringBuilder.setSpan(span.mActualSpan, start, end,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
// Fix flags and range for paragraph-type markup.
obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);
for (Object span : obj) {
int start = mSpannableStringBuilder.getSpanStart(span);
int end = mSpannableStringBuilder.getSpanEnd(span);
// If the last line of the range is blank, back off by one.
if (end - 2 >= 0 && (end - start) >= 2) {
if (mSpannableStringBuilder.charAt(end - 1) == '\n' &&
mSpannableStringBuilder.charAt(end - 2) == '\n') {
end--;
}
}
if (end == start) {
mSpannableStringBuilder.removeSpan(span);
} else {
mSpannableStringBuilder.setSpan(span, start, end, Spannable.SPAN_PARAGRAPH);
}
}
// Remove leading newlines
while (mSpannableStringBuilder.length() > 0
&& mSpannableStringBuilder.charAt(0) == '\n') {
mSpannableStringBuilder.delete(0, 1);
}
// Remove trailing newlines
int last = mSpannableStringBuilder.length() - 1;
while (last >= 0 && mSpannableStringBuilder.charAt(last) == '\n') {
mSpannableStringBuilder.delete(last, last + 1);
last = mSpannableStringBuilder.length() - 1;
}
return mSpannableStringBuilder;
}
private void handleStartTag(String tag, Attributes attributes) {
//noinspection StatementWithEmptyBody
if (tag.equalsIgnoreCase("br")) {
// We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
// so we can safely emit the linebreaks when we handle the close tag.
} else if (tag.equalsIgnoreCase("p")) {
startBlockElement(mSpannableStringBuilder, attributes);
startCssStyle(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("ul")) {
startBlockElement(mSpannableStringBuilder, attributes);
start(mSpannableStringBuilder, new List());
} else if (tag.equalsIgnoreCase("ol")) {
startBlockElement(mSpannableStringBuilder, attributes);
start(mSpannableStringBuilder, new List(parseIntAttribute(attributes, "start", 1)));
} else if (tag.equalsIgnoreCase("li")) {
startLi(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("input")) {
if ("checkbox".equalsIgnoreCase(attributes.getValue("", "type"))) {
@AttrRes int drawableAttrResId = attributes.getIndex("", "checked") >= 0
? R.attr.checkboxCheckedSmallIcon
: R.attr.checkboxUncheckedSmallIcon;
Drawable d = ContextCompat.getDrawable(mContext,
UiUtils.resolveDrawable(mContext, drawableAttrResId));
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM);
mSpannableStringBuilder.append(" ");
mSpannableStringBuilder.setSpan(span, mSpannableStringBuilder.length() - 2,
mSpannableStringBuilder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else if (tag.equalsIgnoreCase("div")) {
startBlockElement(mSpannableStringBuilder, attributes);
String cssClass = attributes.getValue("", "class");
if (cssClass != null && cssClass.indexOf("highlight") == 0) {
start(mSpannableStringBuilder, new CodeDiv());
}
CodeDiv code = getLast(mSpannableStringBuilder, CodeDiv.class);
if (code != null) {
code.mLevel++;
}
} else if (tag.equalsIgnoreCase("span")) {
startCssStyle(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("hr")) {
HorizontalLineSpan span = new HorizontalLineSpan(mDividerHeight, 0x60aaaaaa);
appendNewlines(mSpannableStringBuilder, 2);
int len = mSpannableStringBuilder.length();
mSpannableStringBuilder.setSpan(span, len - 1, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
} else if (tag.equalsIgnoreCase("strong")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("b")) {
start(mSpannableStringBuilder, new Bold());
} else if (tag.equalsIgnoreCase("em")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("cite")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("dfn")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("i")) {
start(mSpannableStringBuilder, new Italic());
} else if (tag.equalsIgnoreCase("big")) {
start(mSpannableStringBuilder, new Big());
} else if (tag.equalsIgnoreCase("small")) {
start(mSpannableStringBuilder, new Small());
} else if (tag.equalsIgnoreCase("font")) {
startFont(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("blockquote")) {
startBlockquote(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("tt")) {
start(mSpannableStringBuilder, new Monospace());
} else if (tag.equalsIgnoreCase("pre")) {
start(mSpannableStringBuilder, new Pre());
CodeDiv div = getLast(mSpannableStringBuilder, CodeDiv.class);
if (div != null) {
div.mHasPre = true;
}
} else if (tag.equalsIgnoreCase("a")) {
startA(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("u")) {
start(mSpannableStringBuilder, new Underline());
} else if (tag.equalsIgnoreCase("del")) {
start(mSpannableStringBuilder, new Strikethrough());
} else if (tag.equalsIgnoreCase("s")) {
start(mSpannableStringBuilder, new Strikethrough());
} else if (tag.equalsIgnoreCase("strike")) {
start(mSpannableStringBuilder, new Strikethrough());
} else if (tag.equalsIgnoreCase("sup")) {
start(mSpannableStringBuilder, new Super());
} else if (tag.equalsIgnoreCase("sub")) {
start(mSpannableStringBuilder, new Sub());
} else if (tag.equalsIgnoreCase("code")) {
boolean inPre = getLast(mSpannableStringBuilder, Pre.class) != null;
if (inPre) {
appendNewlines(mSpannableStringBuilder, 1);
}
start(mSpannableStringBuilder, new Code(inPre));
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
startHeading(mSpannableStringBuilder, attributes, tag.charAt(1) - '1');
} else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
}
}
private void handleEndTag(String tag) {
if (tag.equalsIgnoreCase("br")) {
handleBr(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("p")) {
endCssStyle(mSpannableStringBuilder);
endBlockElement(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("ul")) {
endBlockElement(mSpannableStringBuilder);
end(mSpannableStringBuilder, List.class, null);
} else if (tag.equalsIgnoreCase("ol")) {
endBlockElement(mSpannableStringBuilder);
end(mSpannableStringBuilder, List.class, null);
} else if (tag.equalsIgnoreCase("li")) {
endLi(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("div")) {
endBlockElement(mSpannableStringBuilder);
CodeDiv code = getLast(mSpannableStringBuilder, CodeDiv.class);
if (code != null && --code.mLevel == 0) {
if (code.mHasPre) {
setSpanFromMark(mSpannableStringBuilder, code, new CodeBlockSpan(0x30aaaaaa));
} else {
mSpannableStringBuilder.removeSpan(code);
}
}
} else if (tag.equalsIgnoreCase("span")) {
endCssStyle(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("strong")) {
end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("b")) {
end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("em")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("cite")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("dfn")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("i")) {
end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("big")) {
end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
} else if (tag.equalsIgnoreCase("small")) {
end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
} else if (tag.equalsIgnoreCase("font")) {
endFont(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("blockquote")) {
endBlockquote(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("tt")) {
end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace"));
} else if (tag.equalsIgnoreCase("pre")) {
end(mSpannableStringBuilder, Pre.class, new TypefaceSpan("monospace"));
} else if (tag.equalsIgnoreCase("a")) {
endA(mSpannableStringBuilder);
} else if (tag.equalsIgnoreCase("u")) {
end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
} else if (tag.equalsIgnoreCase("del")) {
end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("s")) {
end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("strike")) {
end(mSpannableStringBuilder, Strikethrough.class, new StrikethroughSpan());
} else if (tag.equalsIgnoreCase("sup")) {
end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
} else if (tag.equalsIgnoreCase("sub")) {
end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
} else if (tag.equalsIgnoreCase("code")) {
Code code = getLast(mSpannableStringBuilder, Code.class);
if (code != null) {
Object backgroundSpan = code.mInPre
? new CodeBlockSpan(0x30aaaaaa) : new BackgroundColorSpan(0x30aaaaaa);
if (code.mInPre) {
appendNewlines(mSpannableStringBuilder, 1);
}
setSpanFromMark(mSpannableStringBuilder, code,
new TypefaceSpan("monospace"), backgroundSpan);
}
} else if (tag.length() == 2 &&
Character.toLowerCase(tag.charAt(0)) == 'h' &&
tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
endHeading(mSpannableStringBuilder);
}
}
private static void appendNewlines(Editable text, int minNewline) {
final int len = text.length();
if (len == 0) {
return;
}
int existingNewlines = 0;
for (int i = len - 1; i >= 0 && text.charAt(i) == '\n'; i--) {
existingNewlines++;
}
for (int j = existingNewlines; j < minNewline; j++) {
text.append("\n");
}
}
private static void startBlockElement(Editable text, Attributes attributes) {
startBlockElement(text, attributes, 2);
}
private static void startBlockElement(Editable text, Attributes attributes, int newlines) {
appendNewlines(text, newlines);
start(text, new Newline(newlines));
String style = attributes.getValue("", "style");
if (style != null) {
Matcher m = getTextAlignPattern().matcher(style);
if (m.find()) {
String alignment = m.group(1);
if (alignment.equalsIgnoreCase("start")) {
start(text, new Alignment(Layout.Alignment.ALIGN_NORMAL));
} else if (alignment.equalsIgnoreCase("center")) {
start(text, new Alignment(Layout.Alignment.ALIGN_CENTER));
} else if (alignment.equalsIgnoreCase("end")) {
start(text, new Alignment(Layout.Alignment.ALIGN_OPPOSITE));
}
}
}
}
private static void endBlockElement(Editable text) {
Newline n = getLast(text, Newline.class);
if (n != null) {
appendNewlines(text, n.mNumNewlines);
text.removeSpan(n);
}
Alignment a = getLast(text, Alignment.class);
if (a != null) {
setSpanFromMark(text, a, new AlignmentSpan.Standard(a.mAlignment));
}
}
private static void handleBr(Editable text) {
text.append('\n');
}
private void startLi(Editable text, Attributes attributes) {
ListItem item = new ListItem(getLast(text, List.class), attributes);
startBlockElement(text, attributes, 1);
start(text, item);
if (item.mOrdered) {
text.insert(text.length(), "" + item.mPosition + ". ");
}
startCssStyle(text, attributes);
}
private void endLi(Editable text) {
endCssStyle(text);
endBlockElement(text);
ListItem item = getLast(text, ListItem.class);
if (item != null) {
if (item.mOrdered) {
text.removeSpan(item);
} else {
setSpanFromMark(text, item, new BulletSpan(mBulletMargin));
}
}
}
private void startBlockquote(Editable text, Attributes attributes) {
startBlockElement(text, attributes);
start(text, new Blockquote());
}
private void endBlockquote(Editable text) {
endBlockElement(text);
end(text, Blockquote.class, new ReplySpan(mReplyMargin, mReplyMarkerSize, 0xffdddddd));
}
private void startHeading(Editable text, Attributes attributes, int level) {
startBlockElement(text, attributes);
start(text, new Heading(level));
}
private static void endHeading(Editable text) {
// RelativeSizeSpan and StyleSpan are CharacterStyles
// Their ranges should not include the newlines at the end
Heading h = getLast(text, Heading.class);
if (h != null) {
setSpanFromMark(text, h, new RelativeSizeSpan(HEADING_SIZES[h.mLevel]),
new StyleSpan(Typeface.BOLD));
}
endBlockElement(text);
}
private static <T> T getLast(Spanned text, Class<T> kind) {
/*
* This knows that the last returned object from getSpans()
* will be the most recently added.
*/
T[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
return objs[objs.length - 1];
}
}
private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
int where = text.getSpanStart(mark);
text.removeSpan(mark);
int len = text.length();
if (where != len) {
for (Object span : spans) {
if (span instanceof LeadingMarginSpan) {
span = new NeedsReversingSpan(span);
}
text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
private static void end(Editable text, Class kind, Object repl) {
Object obj = getLast(text, kind);
if (obj != null) {
setSpanFromMark(text, obj, repl);
}
}
private void startCssStyle(Editable text, Attributes attributes) {
String style = attributes.getValue("", "style");
if (style != null) {
Matcher m = getForegroundColorPattern().matcher(style);
if (m.find()) {
int c = Color.parseColor(m.group(1));
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
m = getBackgroundColorPattern().matcher(style);
if (m.find()) {
int c = Color.parseColor(m.group(1));
if (c != -1) {
start(text, new Background(c | 0xFF000000));
}
}
m = getTextDecorationPattern().matcher(style);
if (m.find()) {
String textDecoration = m.group(1);
if (textDecoration.equalsIgnoreCase("line-through")) {
start(text, new Strikethrough());
}
}
}
}
private static void endCssStyle(Editable text) {
Strikethrough s = getLast(text, Strikethrough.class);
if (s != null) {
setSpanFromMark(text, s, new StrikethroughSpan());
}
Background b = getLast(text, Background.class);
if (b != null) {
setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));
}
Foreground f = getLast(text, Foreground.class);
if (f != null) {
setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));
}
}
private static void startImg(Editable text, Attributes attributes,
android.text.Html.ImageGetter img) {
String src = attributes.getValue("", "src");
Drawable d = img.getDrawable(src);
int len = text.length();
text.append("\uFFFC");
text.setSpan(new ImageSpan(d, src), len, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private void startFont(Editable text, Attributes attributes) {
String color = attributes.getValue("", "color");
String face = attributes.getValue("", "face");
if (!TextUtils.isEmpty(color)) {
int c = Color.parseColor(color);
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
if (!TextUtils.isEmpty(face)) {
start(text, new Font(face));
}
}
private static void endFont(Editable text) {
Font font = getLast(text, Font.class);
if (font != null) {
setSpanFromMark(text, font, new TypefaceSpan(font.mFace));
}
Foreground foreground = getLast(text, Foreground.class);
if (foreground != null) {
setSpanFromMark(text, foreground,
new ForegroundColorSpan(foreground.mForegroundColor));
}
}
private static void startA(Editable text, Attributes attributes) {
String href = attributes.getValue("", "href");
start(text, new Href(href));
}
private static void endA(Editable text) {
Href h = getLast(text, Href.class);
if (h != null) {
if (h.mHref != null) {
setSpanFromMark(text, h, new URLSpan((h.mHref)));
}
}
}
public void setDocumentLocator(Locator locator) {
}
public void startDocument() throws SAXException {
}
public void endDocument() throws SAXException {
}
public void startPrefixMapping(String prefix, String uri) throws SAXException {
}
public void endPrefixMapping(String prefix) throws SAXException {
}
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
handleStartTag(localName, attributes);
}
public void endElement(String uri, String localName, String qName) throws SAXException {
handleEndTag(localName);
}
public void characters(char ch[], int start, int length) throws SAXException {
if (getLast(mSpannableStringBuilder, Pre.class) != null) {
/* We're in a pre block, so keep whitespace intact. */
for (int i = 0; i < length; i++) {
mSpannableStringBuilder.append(ch[i + start]);
}
return;
}
StringBuilder sb = new StringBuilder();
/*
* Ignore whitespace that immediately follows other whitespace;
* newlines count as spaces.
*/
for (int i = 0; i < length; i++) {
char c = ch[i + start];
if (c == ' ' || c == '\n') {
char pred;
int len = sb.length();
if (len == 0) {
len = mSpannableStringBuilder.length();
if (len == 0) {
pred = '\n';
} else {
pred = mSpannableStringBuilder.charAt(len - 1);
}
} else {
pred = sb.charAt(len - 1);
}
if (pred != ' ' && pred != '\n') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
mSpannableStringBuilder.append(sb);
}
public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
}
public void processingInstruction(String target, String data) throws SAXException {
}
public void skippedEntity(String name) throws SAXException {
}
private static int parseIntAttribute(Attributes attributes, String name, int defaultValue) {
String value = attributes.getValue("", name);
if (value != null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
// fall through
}
}
return defaultValue;
}
private static class Bold { }
private static class Italic { }
private static class Underline { }
private static class Strikethrough { }
private static class Big { }
private static class Small { }
private static class Monospace { }
private static class Blockquote { }
private static class Super { }
private static class Sub { }
private static class Pre { }
private static class CodeDiv {
public boolean mHasPre;
public int mLevel;
}
private static class NeedsReversingSpan {
public final Object mActualSpan;
public NeedsReversingSpan(Object actualSpan) {
mActualSpan = actualSpan;
}
}
private static class Code {
public final boolean mInPre;
public Code(boolean inPre) {
mInPre = inPre;
}
}
private static class List {
public final boolean mOrdered;
public int mPosition = 0;
public List() {
mOrdered = false;
}
public List(int position) {
mOrdered = true;
mPosition = position;
}
}
private static class ListItem {
public final boolean mOrdered;
public final int mPosition;
public ListItem(List list, Attributes attrs) {
mOrdered = list != null && list.mOrdered;
int position = list != null ? list.mPosition : -1;
if (mOrdered) {
position = parseIntAttribute(attrs, "value", position);
}
mPosition = position;
if (list != null) {
list.mPosition = position + 1;
}
}
}
private static class Font {
public String mFace;
public Font(String face) {
mFace = face;
}
}
private static class Href {
public String mHref;
public Href(String href) {
mHref = href;
}
}
private static class Foreground {
private int mForegroundColor;
public Foreground(int foregroundColor) {
mForegroundColor = foregroundColor;
}
}
private static class Background {
private int mBackgroundColor;
public Background(int backgroundColor) {
mBackgroundColor = backgroundColor;
}
}
private static class Heading {
private int mLevel;
public Heading(int level) {
mLevel = level;
}
}
private static class Newline {
private int mNumNewlines;
public Newline(int numNewlines) {
mNumNewlines = numNewlines;
}
}
private static class Alignment {
private Layout.Alignment mAlignment;
public Alignment(Layout.Alignment alignment) {
mAlignment = alignment;
}
}
}
}